Babel 插件

您所在的位置:网站首页 butterfly core翻译 Babel 插件

Babel 插件

2023-04-11 04:16| 来源: 网络整理| 查看: 265

开篇

Babel 对于前端开发者来说应该是很熟悉了,它能够转译 ECMAScript 2015+ 的代码,使得代码能够在旧的浏览器或者环境中运行。

Babel 的转换工作可以分为三部分:

Parse(解析):将源代码转换成更加抽象的表示方法(AST 抽象语法树); Transform(转换):对抽象语法树进行变换操作; Generate(代码生成):将转换阶段变换后的抽象语法树,生成新的源代码。

其中,Babel 插件应用于第二阶段 Transform。

大多数插件的指责是转换代码,但除了这项重要工作外,插件还可以用于新的工作任务,比如用于处理 收集 工作。

业务背景

通常公司会有一个专门管理词条的数据池,包含了基础通用词条及和本工程相关词条,如果将词条资源全部引入,会造成引入体积过大,影响页面性能。

所以本篇围绕的主题:通过收集项目中使用到的 i18n 多语种词条 key,去词条数据池中查找与之对应的数据,生成最终的词条资源(按需打包词条资源)。

下面我们先来熟悉一下如何编写一个插件。

插件基本结构

插件本身是一个函数,函数的入参是 babel 对象,从中我们可以拿到 babel 的所有成员,最常用的是 types 对象,可以通过它来构造、变换 AST 节点。

如下是一个插件的基本结构:返回一个对象,其中访问者(visitor)的内容对应的是一个个 AST 节点类型,在遍历 AST 节点时,就会进入 visitor 中去匹配相应类型。

export default function ({ types: t }) { return { pre(file) { this.cache = new Map(); }, visitor: { ... }, post(file) { console.log(this.cache); } }; } 复制代码

pre 和 post 分别会在遍历 visitor 前后调用,接收 file(当前要处理的文件)作为参数,通过 file.opts.filename 拿到文件路径;一般在这里可以做一些插件调用前后的逻辑。

注意,插件的函数体结构之后在构建时执行一次;但函数 return 的这个对象,会在每个文件中都执行一次完整的生命周期,即:在处理每个文件时都会执行 pre、visitor、post 属性方法。

代码演示

通常,我们在项目中可以通过 i18n 函数表达式接收词条 key 来渲染多语种文本,具体如下:

function Button() { return ( {i18n('save')} ) } 复制代码

场景: 所有项目工程内的词条统一由一个数据存储池来集中管理。(可以是一个 git 仓库)

期望: 在经过打包后,收集到本项目中所有 i18n 函数表达式的参数 key,与我们的词条存储池中的数据进行匹配,最终生成一个只包含本项目内所使用到的相关词条。

这里,我们先以 babel-plugin-collect-i18n 来表示插件名称。

我们可以这样使用,比如在 .babelrc.js 中:

module.exports = { // ... plugins: [ ... other plugins ['babel-plugin-collect-i18n', { mode: 'generate', name: ['i18n'], output: 'src/locale/i18n.js', moduleType: 'es', locale: 'i18n/index.json', }], ], } 复制代码

参数:

mode: 模式,可选值有 generate(生成)和 `collect(收集),默认 generate; name: 表达式名称,比如这里的 i18n,当要匹配多个表达式时,可以传递数组,如:['i18n', 'i19n']; output: 输出路径,收集或生成后存放路径,默认存放在 src/locale/i18n.js 文件下; moduleType: 输出模块类型,可选值有 commonjs 和 es,默认为 es; locale: 数据池所在位置,JSON 文件格式,当 mode = generate 时会用到,默认查找 i18n/index.json 文件;

数据池数据结构:

注意,是一个 JSON 文件

// i18n/index.json { "base.save": { "zh-cn": "保存", "en": "Save" }, "base.cancel": { "zh-cn": "取消", "en": "Cancel" }, "base.sure": { "zh-cn": "确认", "en": "OK" } } 复制代码

输出:

// src/locale/i18n.js export default { "save": { "zh-cn": "保存", "en": "Save" } } 复制代码 具体实现 1、首先搭建插件基本结构: const fs = require('fs'); const path = require('path'); module.exports = () => { const collector = new Map(), noMatchKeys = []; let options = null; return { pre() { // ... }, visitor: { // ... }, post() { // ... } } }; 复制代码 2、i18n() 表达式的 AST 结构:

我们在 AST explorer 可以看到,i18n 的 AST 结构如下:

1650544262151.jpg

图中标记的 type 属性值将作为插件 visitor 中要处理的节点类型:

module.exports = () => { ... return { ... visitor: { CallExpression(path, state) { ... } }, ... } }; 复制代码 3、记录插件参数

如果使用的 Babel v7 版本,我们可以通过插件函数拿到在使用插件时传递的配置信息:

module.exports = (babel, options, dirname) => { // 插件配置信息:options }; 复制代码

但是 Babel v6 版本无法通过这种方式拿到配置信息;为了兼容两者,统一在 visitor 遍历到表达式时,通过 state 参数重获取配置信息:

module.exports = () => { const collector = new Map(), noMatchKeys = []; let options = null; return { ... visitor: { CallExpression(path, state) { if (!options) options = mergePluginOptions(state.opts); collectI18nKeys(path, options, collector); } }, ... } }; const mergePluginOptions = (options = {}) => { return { // 模式:只做收集 | 收集并匹配词条输出词条文件 mode: options.mode === 'collect' ? 'collect' : 'generate', // i18n 表达式参数 key name: options.name ? (Array.isArray(options.name) ? options.name : [options.name]) : ['i18n'], // 输出路径 output: options.output || 'src/locale/i18n.js', // 输出模块类型 moduleType: options.moduleType === 'commonjs' ? 'commonjs' : 'es', // 与词条 key 相匹配的多语种资源所在位置 locale: options.locale || 'i18n/index.json', } } 复制代码 4、收集 i18n key

我们建立一个 Map 对象用作收集存储池,接下来就是匹配 CallExpression 后的收集工作:

const collector = new Map(); 复制代码

由于 i18n 是一个方法,可能会被注册在全局 window 上,因此考虑两种场景。在匹配到 i18n 后,我们只需要存储函数的参数,也就是我们要收集的 key:

const collectI18nKeys = (path, options, collector) => { const node = path.node; if ( // 1、使用 i18n('xxx') 方式 (标识符) (node.callee.type === 'Identifier' && options.name.indexOf(node.callee.name) > -1 && node.arguments.length > 0) || // 2、使用 window.i18n('xxx') 方式 (对象成员表达式) (node.callee.type === 'MemberExpression' && node.callee.object.name === 'window' && options.name.indexOf(node.callee.property.name) > -1 && node.arguments.length > 0) ) { const argNode = node.arguments[0]; // 第一个参数 ast 节点 let i18nKey = null; if (argNode.test) { // 情况2:i18n(bool ? 'xxx' : 'xxx') (条件参数:三目运算符) i18nKey = `${argNode.consequent.value},${argNode.alternate.value}`; } else { // 情况1:i18n('xxx') i18nKey = argNode.value; } i18nKey.split(',').forEach(key => { if (!collector.has(key)) collector.set(key, true); }); // const i18nKey = node.arguments[0].value; // if (!collector.has(i18nKey)) collector.set(i18nKey, true); } } 复制代码

注意,参数在使用三目运算符时,可能存在两个 key,都要做收集。

5、进行输出

上面通过 visitor 收集到了我们的词条 keys,那在什么时机将这些 keys 写入到 output 输出文件呢?

我们无法检测 Babel 插件何时工作执行完成,但是我们可以借助插件的 pre 和 post 两个方法实现一些写入逻辑。

Babel 插件在处理每个文件时,都会先执行 pre 方法,然后遍历文件内所有节点去 visitor 中匹配类型,最后执行 post 方法。

但是,如果在某个文件内并未收集到 i18n 表达式,其实是没有必要去做 keys 写入工作的,因此我们可以通过 this.collectorTotal 来做优化:

module.exports = () => { const collector = new Map(), noMatchKeys = []; let options = null; return { pre() { this.collectorTotal = collector.size; }, visitor: { ... }, post() { if (this.collectorTotal !== collector.size) { // ... } } } }; 复制代码

现在,我们可以解析外部传入的 options 参数,进行输出。

post() { // 在文件内收集到了 i18n,输出到目录文件中 if (this.collectorTotal !== collector.size) { const { mode, output, moduleType, locale } = options; const outputDirectory = path.resolve(path.dirname(output)), outputFile = path.resolve(output); const exportType = moduleType === 'commonjs' ? `module.exports =` : `export default`; const i18nKeys = Array.from(collector.keys()); let result = {}; if (mode === 'generate') { const localeContent = JSON.parse(fs.readFileSync(path.resolve(locale), 'utf8')); for (let i = 0; i < i18nKeys.length; i++) { const key = i18nKeys[i], value = localeContent[key]; if (!value && noMatchKeys.indexOf(key) === -1) { noMatchKeys.push(key); continue; } result[key] = value; } if (noMatchKeys.length > 0) { console.log("\n \033[33m " + `warning: ${noMatchKeys.join(', ')} not found in locale file.` + "\033[0m \n"); } } else { result = i18nKeys; } const content = `${exportType} ${JSON.stringify(result, null, 2)}`; // 确保目录存在 try { fs.statSync(outputDirectory) } catch { fs.mkdirSync(outputDirectory) } fs.writeFileSync(outputFile, content); } } 复制代码 首先,我们可以通过 collector 拿到收集到的 keys; 如果 mode=collect,我们只需要将收集到的 keys 写入到 options.output 即可; 如果 mode=generate,我们会读取外部的 options.locale 数据池文件中的数据,将 keys 和数据池中数据继续匹配,最终匹配到的就是项目中所有使用到的词条资源; 注意这里还使用到了一个变量:noMatchKeys,当我们工程内使用的 key 不在数据池中时,可以暴露出去,告知用户。 6、优化输出性能

从上面输出写入文件逻辑得知:如果一个文件中存在我们要收集的 key,那么在这个文件扫描结束后,会进行一次写入操作;

试想,加入工程内有 100 个文件都使用到了 i18n,那么就会执行 100次 写入操作,这个开销非常巨大。

这里我们可以借助 防抖 来提升性能:例如以 300ms 为间隔进行写入:

let timer = null; post() { if (this.collectorTotal !== collector.size) { clearTimeout(timer); timer = setTimeout(() => { ... }, 300) } } 复制代码 最后

至此,我们通过一个 Babel 插件,实现了前端工程 i18n 多语种按需打包方案。

感谢阅读。

Github 地址:babel-plugin-collect-i18n



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3